// Copyright 2009 Google Inc. All Rights Reserved.
/**
 * @fileoverview The main file starting group status gadget.
 * @author majewski@google.com (Bo Majewski)
 */

gadgets.util.registerOnLoadHandler(initWave);

// Global constants
var MILLIS_PER_DAY = 1000 * 60 * 60 * 24;
var MILLIS_PER_MINUTE = 1000 * 60;

// Global variables.
var groupStatus = GroupStatus();
var hintShowing = true;
var stateReceived = false;
var participantsReceived = false;
var $statusDisplay;

function postStatus(userId, userState) {
  delta = {
    changed: userId
  };
  delta[userId] = (userState == "[]") ? null : userState;
  wave.getState().submitDelta(delta);
}

function onStateChanged() {
  stateReceived = true;
  var state = wave.getState();
  var id = null; // state.get("changed");
  if (id) {
    var delta = state.get(id);
    groupStatus.update(id, jQuery.parseJSON(delta));
  } else {
    var keys = state.getKeys();
    for (var keyIndex in keys) {
      var userId = keys[keyIndex];
      if (userId.indexOf('@') != -1) {
        var userState = state.get(userId);
        try {
          var activities = jQuery.parseJSON(userState);
          groupStatus.update(userId, activities);
        } catch (error) {
          wave.log("Error processing " + userId + ":" + userState);
        }
      }
    }
  }
  render();
}

function onParticipantChanged() {
  participantsReceived = true;
  render();
}

function initWave() {
  initUI();
  if (wave && wave.isInWaveContainer()) {
    wave.setStateCallback(onStateChanged);
    wave.setParticipantCallback(onParticipantChanged);
  }
}

function toShortDate(when) {
  return when.getFullYear() + "/" + (1 + when.getMonth()) + "/" + when.getDate();
}

function toShortTime(when) {
  var hours = when.getHours();
  var minutes = toFixedWidthString(when.getMinutes(), 2);
  var lang = navigator.language;
  if (lang != null && lang.match(/en.*/)) {
    return (hours > 12) ? (hours - 12) + ":" + minutes + " pm" : hours + ":" + minutes + " am";
  }
  return hours + ":" + minutes;
}

function toLocalDate(when) {
  var year = when.getFullYear();
  var str = when.toLocaleDateString();
  var pattern = RegExp("[\\s,/.]*" + year + "[\\s,/.]*");
  return str.replace(pattern, "");
}

function getTimeStamp(startTime, endTime, showStartDate) {
  if (startTime.getHours() == 0 && startTime.getMinutes() == 0
      && endTime.getHours() == 0 && endTime.getMinutes() == 0) {
    return showStartDate ? "(" + toShortDate(startTime) + ")" : "";
  }
  var str = "(";
  if (showStartDate) {
    str += toShortDate(startTime) + ", ";
  }
  var workStartTime = new Date(startTime.getTime());
  if (workStartTime.getHours() < 8) {
    workStartTime.setHours(8);
  }
  var workEndTime = new Date(endTime);
  if (workEndTime.getHours() > 20 || workEndTime.getHours() == 0) {
    workEndTime.setHours(20);
  }
  return "(" + toShortTime(workStartTime) + " - " + toShortTime(workEndTime) + ")";
}

function fromISOString(str) {
  var part = str.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):\d{2}.*Z/);
  if (part == null) {
    return null;
  }
  var when = new Date(part[1], parseInt(part[2]) - 1, part[3], part[4], part[5]);
  when.setTime(when.getTime() - MILLIS_PER_MINUTE * when.getTimezoneOffset());
  return when;
}

function toFixedWidthString(value, digits) {
  var str = value.toString();
  while (str.length < digits) {
    str = "0" + str;
  }
  return str;
}

function UserStatus(startTime, endTime, statusText) {
  return {
    startTime: startTime.toISOString(),
    endTime: endTime.toISOString(),
    statusText: statusText,

    hasSameDuration: function(start, end) {
      return this.getStartTime().getTime() == start.getTime()
          && this.getEndTime().getTime() == end.getTime();
    },

    getStatusText: function() {
      return this.statusText;
    },

    getStartTime: function() {
      return fromISOString(this.startTime);
    },

    getEndTime: function() {
      return fromISOString(this.endTime);
    },

    toString: function() {
      return this.startTime + "%" + this.endTime + "%" + this.statusText;
    }
  };
}

function GroupStatus() {
  return {
    // A map from UTC date to user status
    userMap: {},
    callbacks: [],

    toDateKey: function(when) {
      return when.getFullYear() + toFixedWidthString(when.getMonth(), 2)
          + toFixedWidthString(when.getDate(), 2);
    },

    add: function(id, when, what) {
      var startDate = new Date(when.getFullYear(), when.getMonth(), when.getDate());
      var endDate = new Date(startDate.getTime() + MILLIS_PER_DAY);
      var activities = this.userMap[id];
      if (activities == null) {
        activities = [];
        this.userMap[id] = activities;
      }
      var foundAt = -1;
      for (var i = 0; i < activities.length; ++i) {
        var activity = activities[i];
        if (activity.hasSameDuration(startDate, endDate)) {
          foundAt = i;
          break;
        }
      }
      if (foundAt == -1) {
        if (what) {
          activities.push(UserStatus(startDate, endDate, sanitize(what)));
        }
      } else {
        if (!what) {
          activities.splice(foundAt, 1);
          if (activities.length == 0) {
            delete this.userMap[id];
          }
        } else {
          activities[foundAt] = UserStatus(startDate, endDate, sanitize(what));
        }
      }
      this.fireChange_(id, activities);
    },

    fill: function(dateBuckets) {
      for (userId in this.userMap) {
        if (!this.userMap.hasOwnProperty(userId)) {
          continue;
        }
        var activities = this.userMap[userId];
        for (var index in activities) {
          var activity = activities[index];
          var start = activity.getStartTime();
          var end = activity.getEndTime();
          var bucketId = -1;
          for (var i = 0; i < dateBuckets.length; ++i) {
            var bucket = dateBuckets[i];
            if (start >= bucket.when && end > bucket.when) {
              bucketId = i;
              break;
            }
          }
          if (bucketId == -1) {
            continue;
          }
          var bucket = dateBuckets[bucketId];
          var until = (end < bucket.when) ? end : bucket.when;
          bucket.statuses.push({
              id: userId, start: start, end: until, what: activity.getStatusText()
          });
          if (bucketId > 0) {
            var preceeding = dateBuckets[bucketId - 1];
            if (end > preceeding.when) {
              preceeding.statuses.push({
                  id: userId, start: preceeding.when, end: end, what: activity.getStatusText()
              });
            }
          }
        }
      }
    },

    update: function(id, vector) {
      var activities = [];
      for (var index in vector) {
        var serialized = vector[index];
        var part = serialized.match(/([^%]+)%([^%]+)%(.+)/);
        if (part == null) {
          continue;
        }
        var startTime = fromISOString(part[1]);
        var endTime = fromISOString(part[2]);
        if (startTime != null && endTime != null && part[3] != null) {
          activities.push(UserStatus(startTime, endTime, part[3]));
        }
      }
      if (activities.length == 0) {
        delete this.userMap[id];
      } else {
        this.userMap[id] = activities;
      }
    },

    purge: function(deadline) {
      changed = {};
      for (var userId in this.userMap) {
        var activities = this.userMap[userId];
        var i = 0;
        var modified = false;
        while (i < activities.length) {
          var activity = activities[i];
          if (activity.getEndTime() < deadline) {
            modified = true;
            activities.splice(i, 1);
          } else {
            ++i;
          }
        }
        if (modified) {
          changed[userId] = activities;
          if (activities.length == 0) {
            delete this.userMap[userId];
          }
        }
      }
      for (userId in changed) {
        this.fireChange_(userId, changed[userId]);
      }
    },

    addCallback: function(callback) {
      this.callbacks.push(callback);
    },

    fireChange_: function(id, activities) {
      var sep = '';
      var str = "[";
      for (var i = 0; i < activities.length; ++i) {
        str += sep + '"' + activities[i].toString() + '"';
        sep = ',';
      }
      str += ']';
      for (var index in this.callbacks) {
        this.callbacks[index](id, str);
      }
    },

    toString: function() {
      return this.map.toString();
    }
  };
}

function initUI() {
  // Get the input field ready for user input.
  $("#statusinput").focus(
    function (evt) {
      if (hintShowing) {
        hintShowing = false;
        this.className = 'status_post';
        this.value = '';
      }
    }
  );

  // Show helpful message for blurred status input.
  $("#statusinput").blur(
    function (evt) {
      if (!hintShowing && !this.value) {
        hintShowing = true;
        this.className = 'status_pre';
        this.value = 'my status...';
      }
    }
  );

  // Watches changes in the status input field.
  $("#statusinput").keydown(
    function (evt) {
      if (evt.keyCode == 0xd) {
        var newStatus = this.value;
        var count = $("#rep").val();
        var when = new Date($("#dayvalue").val());
        for (; count > 0; --count) {
          groupStatus.add(wave.getViewer().getId(), when, newStatus);
          when.setTime(when.getTime() + MILLIS_PER_DAY);
        }
        this.value = '';
        render();
      }
    }
  );
  $("#statusinput").val("my status...");
  $statusDisplay = $("#statusdisplay").tabs({
      selected: -1,
      show: function(envet, ui) { gadgets.window.adjustHeight(); }
  });
  $("#day").datepicker({
      showAnim: "",
      dateFormat: "D, d M",
      altField: '#dayvalue',
      altFormat: 'yy/mm/dd',
      minDate: 0,
      maxDate: '1M'
  });
  var now = new Date();
  $("#day").val($.datepicker.formatDate('D, d M', now));
  $("#dayvalue").val(toShortDate(now));
  groupStatus.addCallback(postStatus);
  window.setTimeout(cleanup, 1000 * (5 + parseInt(Math.random() * 55)));
}

function cleanup() {
  var deadline = new Date();
  deadline.setTime(deadline.getTime() - 2 * MILLIS_PER_DAY);
  wave.log("Starting cleanup of activities ending beofe " + deadline.toString());
  groupStatus.purge(deadline);
}

function sanitize(str) {
  var safe = "";
  for (var i = 0; i < str.length; ++i) {
    var c = str[i];
    switch (c) {
      case '<': safe += "&lt;"; break;
      case '>': safe += "&gt;"; break;
      case '&': safe += "&quot;"; break;
      case '\\': safe += '\\\\'; break;
      default: safe += c; break;
    }
  }
  return safe;
}

function render() {
  if (!participantsReceived || !stateReceived) {
    return;
  }
  var profiles = {};
  var participants = wave.getParticipants();
  for (var index in participants) {
    profiles[participants[index].getId()] = participants[index].getDisplayName();
  }
  var instant = new Date();
  var when = new Date(instant.getFullYear(), instant.getMonth(), instant.getDate());
  when.setTime(when.getTime() + 2 * MILLIS_PER_DAY);
  var dateBuckets = [
    {id: "#future", when: null, statuses: []},
    {id: "#tomorrow", when: null, statuses: []},
    {id: "#today", when: null, statuses: []},
    {id: "#yesterday", when: null, statuses: []}
  ];
  for (var index in dateBuckets) {
    dateBuckets[index].when = new Date(when.getTime());
    when.setTime(when.getTime() - MILLIS_PER_DAY);
  }
  groupStatus.fill(dateBuckets);
  var maxActivitiesPerBucket = 0;
  for (var i in dateBuckets) {
    var bucket = dateBuckets[i];
    bucket.statuses.sort(function(a, b) {
      var p = profiles[a.id];
      var q = profiles[b.id];
      return (p < q) ? -1 : ((p > q) ? 1 : 0);
    });
    html = "";
    var activityCount = 0;
    for (var j in bucket.statuses) {
      var activity = bucket.statuses[j];
      var startHour = activity.start.getHours();
      var endHour = activity.end.getHours();
      if (endHour == 0) {
        endHour = 24;
      }
      if (startHour < 20 && endHour > 8) {
        var timeStr = getTimeStamp(activity.start, activity.end, i == 0);
        html += '<li class="statusline">'
            + '<span class="name" title="' + activity.id + '">'
            + profiles[activity.id] + '</span>'
            + ': '
            + '<span class="status">' + activity.what + '</span>'
            + ' <span class="time">' + timeStr + '</span>'
            + '</li>';
        ++activityCount;
      }
    }
    maxActivitiesPerBucket = Math.max(maxActivitiesPerBucket, activityCount);
    $(bucket.id).empty().append('<ul>' + html + '</ul>');
  }
  $("#statistics").text(statisticsHtml.join(""));
  if (-1 == $statusDisplay.tabs('select')) {
    $statusDisplay.tabs('select', 1);
  }
  gadgets.window.adjustHeight();
}
